jqを活用してAPIレスポンス等から欲しい情報だけを抽出する【初級編】
よく訓練されたアップル信者、都元です。jq
というツールはご存知でしょうか。ご存じない方は、まずはこの辺りのエントリーを御覧ください。
jq
は要するに、入力したJSONに何らかの加工をして、その結果のJSONを出力するツールです。どのような加工をするのかを指定するためのクエリ言語があり、それを解釈して実行する役割を持っています。
AWSをはじめとして、各種Web APIのレスポンスはJSONで取り扱うことが多く、そのJSONから欲しい情報のみをリストアップしたい、というニーズは常に存在します。
例えば aws ec2 describe-instances
コマンドを使えば、全てのEC2インスタンスの情報が一度に取れます。筆者が扱うとある環境でコマンドを実行してみたところ、2500行を越えるJSONが返って来ました。
そもそもインスタンスは何台動いているのだろうか。あのインスタンスのプライベートIPアドレスを知りたい。などなど、ある目的を持ってコマンドを叩くわけですが、2500行ものJSONから目的の情報にたどり着くコストは馬鹿になりません。
こんな時、jq
を使って必要な情報のみを抽出すると、色々仕事がしやすくなります。というわけでjq
におけるクエリを皆さんに自由自在に書いてもらうべく、実例を交えながら解説をしていきたいと思います。
JSONの整形
上記で例に挙げたaws
コマンドは、ありがたいことにJSONが整形された状態で出力されます。しかし、下記のOpenWeatherMapのように、JSONをベタっと返してくるAPIも少なく有りません。
$ curl -s http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp {"coord":{"lon":139.69,"lat":35.69},"sys":{"type":1,"id":7622,"message":0.0272,"country":"JP","sunrise":1435692536,"sunset":1435744872},"weather":[{"id":521,"main":"Rain","description":"shower rain","icon":"09d"},{"id":701,"main":"Mist","description":"mist","icon":"50d"}],"base":"stations","main":{"temp":295.7,"pressure":993,"humidity":88,"temp_min":292.59,"temp_max":299.26},"visibility":11265,"wind":{"speed":4.1,"deg":360},"rain":{"1h":1.27},"clouds":{"all":90},"dt":1435725582,"id":1850147,"name":"Tokyo","cod":200}
執筆時点では上記の通り自由に気象情報が取れましたが、残念ながら現時点ではサインアップ(無料)の上APIキーを取得する必要があります。
jq
ではこのようなJSONに適切な改行とインデントを挿入して処理できます。
$ curl -s http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp | jq '.' { "coord": { "lon": 139.69, "lat": 35.69 }, "sys": { "type": 1, "id": 7622, "message": 0.0272, "country": "JP", "sunrise": 1435692536, "sunset": 1435744872 }, "weather": [ { "id": 521, "main": "Rain", "description": "shower rain", "icon": "09d" }, { "id": 701, "main": "Mist", "description": "mist", "icon": "50d" } ], "base": "stations", 略
ここでパイプ経由で呼び出しているjq
コマンドの引数.
というのがjqのクエリで、入力値を(加工せず)そのまま出力せよ、という意味です。実際「そのまま」ではなくインデント挿入はされていますが、JSONの値としては無加工で、等価です。
オブジェクトの部分的抽出(オブジェクトクエリ)
上記のJSONのうち、coord
に対する値だけを取り出したい、つまり下記のような出力を得たい場合、どのようなクエリを書けば良いでしょうか。
{ "lon": 139.69, "lat": 35.69 }
予備知識なしでは分からないと思うのでいきなり答えですが、.coord
です。
では、さらにその中のlon
に対する値だけ、つまり139.69
を取り出したいときは、こうです。
$ curl -s http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp | jq '.coord | .lon' 139.69
複数のクエリを|
で結合することによって、前段の出力に対して更なる変換を掛ける、という技です。ちなみにこの表現 .coord | .lon
は .coord.lon
という感じで略記可能です。知っている人は、こちらのほうが馴染みが深いと思います。が、複数つのクエリが合成されたものなんだ、という認識ができると、複雑なクエリを理解しやすい粒度まで分割して考えられるようになるので、知っておくことをオススメします。
配列の部分的抽出(配列クエリ)
$ curl -s http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp | jq '.weather' [ { "id": 521, "main": "Rain", "description": "shower rain", "icon": "09d" }, { "id": 701, "main": "Mist", "description": "mist", "icon": "50d" } ]
さて、上記のように、weather
に対する値は、配列です。中には2つのオブジェクトが入っていますが、この1つ目だけを取り出したい。その場合は .[0]
です。インデックスを指定すればOK。つまり.weather | .[0]
ですね。これは .weather[0]
という風に略記できます。こちらのほうが直感的ですね。
今回の例だと要素が2つだけなので例示しづらいですが、もっと要素数が多い場合[3:5]
という記述では、4番目〜6番目の要素を含む配列を取り出せます。また[-1:]
だと最後の1要素だけを含む配列を取り出します。
と説明しておいてアレなんですが、この技はあんまり使いません。データが配列で渡される状況でありながら、特定のインデックスの要素だけが重要である、ということは経験上あまり無いです。まぁ、このセクションは教養として。
配列の各要素に対して、それぞれクエリを適用したい(配列の展開)
具体的には。上記の.weather
の中のmain
の値が全部欲しい時。期待する出力はコレです。
"Rain" "Mist"
残念ながら.weather.main
ではエラーになってしまいます。配列に対して.main
というクエリは適用できないからです。ここで使うのが.[]
という表記です。
$ curl -s http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp | jq '.weather' [ { "id": 521, "main": "Rain", "description": "shower rain", "icon": "09d" }, { "id": 701, "main": "Mist", "description": "mist", "icon": "50d" } ] $ curl -s http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp | jq '.weather | .[]' { "id": 521, "main": "Rain", "description": "shower rain", "icon": "09d" } { "id": 701, "main": "Mist", "description": "mist", "icon": "50d" }
上記の2つの出力を見比べてみてください。.[]
を使うと、一番外側の配列を外して展開する効果があります。勘の良い方はすでにお気づきかとは思いますが、これにも略記があり、.weather[]
と書けます。
さてここで一つ訂正があります。先ほど
jq
は要するに、入力したJSONに何らかの加工をして、その結果のJSONを出力するツールです。
と紹介しましたが、正確には「入力したJSON列に何らかの加工をして、その結果のJSON列を出力するツール」です。
以前非エンジニアに贈る「具体例でさらっと学ぶJSON」で説明しましたが、それによれば .weather[]
の出力結果はJSONではありません。複数のJSONが連なったもので、本エントリではこれをJSON列と呼ぶことにします。(一般用語ではありません。このエントリ内での呼び方です。)
ちなみにJSON自体は個数が1つのJSON列と考えることができますので、JSONもJSON列の一種です。わかりづらいっすかね。
さて閑話休題。欲しいのはコレ、という路線に戻しますね。
"Rain" "Mist"
というわけで .weather[]
の結果出てきたJSON列に対して .main
というクエリをぶつけてみます。複数のクエリを順番に適用するには |
で連結でしたね。でも略記がありそうですね? というわけで .weather[].main
です。
最終結果はJSON列じゃなくてJSONとして得たい(配列の構築)
ハイ。ここで得られた結果はJSON列(文字列もJSONの一種なので、文字列が2つ連なったものは、JSON列の一種)です。でもJSON列というのはあまり一般的なものではありません。後続の処理の都合上、結果はJSON列じゃなくてJSONとして得たいことがあります。つまり、こう。
[ "Rain", "Mist" ]
得られた結果を、全体的に配列記号で括りたい。言って見れば .[]
の逆操作がしたい。ならば全体を配列記号でくくってみればいいじゃない。ということで [.weather[].main]
です。ちょっと新しいパターンですね。今まではパイプでつないで次の関数を適用する、っていうイメージでしたが、全体に掛かるような式の書き方は初めてだと思います。が、そういうものだと思って頂けると。ちなみにこの操作を配列の構築(Array construction)と呼びます。
$ curl -s http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp | jq '[.weather[].main]' [ "Rain", "Mist" ]
さて、一瞬だけ上級編な話を挟ませてください。上のクエリは、配列をバラして、.main
という関数を適用して、その後再び配列の構築をしています。えー、それって配列に対するmapじゃん? と思う人は思うと思います。ということで、上記はこんな書き方もできます。
$ curl -s http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp | jq '.weather | map(.main)' [ "Rain", "Mist" ]
はい、一旦難しい話終わり。
最終結果はJSON配列じゃなくてJSONオブジェクトとして得たい(オブジェクトの構築)
で、JSONに対する処理の流れでは、結果がJSON配列であるのも不都合かもしれません。オブジェクトとして得たい場合はどうしましょうか。つまり、得たい結果はコレ。
{ "tenki": [ "Rain", "Mist" ] }
これも上記で「配列にしたければ[x]
でいいじゃん」と同じノリです。オブジェクトにしたければ{tenki:x}
でいいわけです。xの部分を今まで作ってきた式に置き換えて…。
$ curl -s http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp | jq '{tenki:[.weather[].main]}' { "tenki": [ "Rain", "Mist" ] }
という感じです。
まとめ
さて、いかがでしたでしょうか。
- オブジェクトクエリ
.xxx
とクエリの合成|
- 配列クエリ
.[n]
や.[n:m]
- 配列の展開
.[]
- 配列の構築
[xxx]
- オブジェクトの構築
{xxx:yyy}
と5つのjq
基本操作をご紹介しました。ここまでの知識を持っていれば、「自分のほしい形式にキッチリ落としこむ」ことはまだ難しいかもしれませんが「(どんな形式であれ)自分の欲しい情報だけを切り出す」ことができるようになるはずです。